package org.bartcr.tools.io.scanner;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.filefilter.AndFileFilter;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.IOFileFilter;

import java.io.File;
import java.io.IOException;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * A scanner to find classes on the classpath.
 * <p/>
 * The basic scanner will list all classes:
 * <p/>
 * <pre>{@code
 * new ClasspathScanner().list(); // Get all classes on the classpath
 * new ClasspathScanner(customClassLoader).list(); // Get all classes from the custom loader
 * }</pre>
 * <p/>
 * The scanner can be set up with processor's to perform some tasks on these classes.
 * <p/>
 * Filters can be added to the scanner to get a more fine-grained selection of classes.
 */
public final class ClasspathScanner {

    private final URLClassLoader classLoader;
    private final AndFileFilter fileFilter;
    private final IOFileFilter classFileNameFilter;

    private boolean includeJDKClasses = false;

    public ClasspathScanner() {
        this((URLClassLoader) Thread.currentThread().getContextClassLoader());
    }

    public ClasspathScanner(URLClassLoader classLoader) {
        this.classLoader = classLoader;

        this.fileFilter = new AndFileFilter();
        this.classFileNameFilter = FileFilterUtils.suffixFileFilter("class");

        this.fileFilter.addFileFilter(classFileNameFilter);
    }

    /**
     * Tell the scanner whether to include classes from the JDK or not. This is turned off by default.
     */
    public ClasspathScanner includeJDKClasses(boolean include) {
        this.includeJDKClasses = include;
        return this;
    }

    /**
     * @return the set of class names compiled by this ClasspathScanner
     */
    public List<String> list() {
        List<String> result = new ArrayList<String>();

        List<File> resources = getResourcesToScan();
        for (File resource : resources) {
            if (resource.isFile()) { // Should be a JAR file
                result.addAll(scanJarFile(resource));
            } else if (resource.isDirectory()) {
                result.addAll(scanDirectory(resource));
            }
        }
        return result;
    }

    private List<File> getResourcesToScan() {
        List<File> result = new ArrayList<File>();

        File[] files = FileUtils.toFiles(classLoader.getURLs());
        for (File file : files) {
            if (includeJDKClasses || !file.getAbsolutePath().startsWith(System.getProperty("java.home"))) {
                result.add(file);
            }
        }
        return result;
    }

    private List<String> scanJarFile(File jarFile) {
        List<String> result = new ArrayList<String>();
        try {
            ZipFile zipFile = new ZipFile(jarFile);
            Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();

            while (zipEntries.hasMoreElements()) {
                String name = zipEntries.nextElement().getName();
                if (classFileNameFilter.accept(null, name)) {
                    result.add(FilenameUtils.removeExtension(name).replaceAll("[\\\\/]", "."));
                }
            }
        } catch (IOException ignore) {
        }
        return result;
    }

    private List<String> scanDirectory(File baseDir) {
        List<String> result = new ArrayList<String>();
        ScannerDirectoryWalker walker = new ScannerDirectoryWalker(FileFilterUtils.trueFileFilter(), fileFilter);
        Set<File> files = walker.list(baseDir);
        for (File file : files) {
            result.add(changeFileNameToClassName(baseDir, file));
        }
        return result;
    }

    /**
     * Change the passed file to its corresponding class name, using basedir as the root directory for the class.
     *
     * @param baseDir   the root directory for the class
     * @param classFile the file containing the class
     */
    private static String changeFileNameToClassName(File baseDir, File classFile) {
        return FilenameUtils.removeExtension(classFile.getAbsolutePath()).substring(baseDir.getAbsolutePath().length() + 1).replaceAll("[\\\\/]", ".");
    }

}
